import classNames from 'classnames'
import warning from 'warning'
import PropTypes from '../../_util/vue-types'
import BaseMixin from '../../_util/BaseMixin'
import { initDefaultProps, hasProp } from '../../_util/props-util'
import Track from './common/Track'
import createSlider from './common/createSlider'
import * as utils from './utils'

const rangeProps = {
  defaultValue: PropTypes.arrayOf(PropTypes.number),
  value: PropTypes.arrayOf(PropTypes.number),
  count: PropTypes.number,
  pushable: PropTypes.oneOfType([
    PropTypes.bool,
    PropTypes.number,
  ]),
  allowCross: PropTypes.bool,
  disabled: PropTypes.bool,
  tabIndex: PropTypes.arrayOf(PropTypes.number),
  prefixCls: PropTypes.string,
}
const Range = {
  name: 'Range',
  displayName: 'Range',
  mixins: [BaseMixin],
  props: initDefaultProps(rangeProps, {
    count: 1,
    allowCross: true,
    pushable: false,
    tabIndex: [],
  }),
  data () {
    const { count, min, max } = this
    const initialValue = Array.apply(null, Array(count + 1))
      .map(() => min)
    const defaultValue = hasProp(this, 'defaultValue') ? this.defaultValue : initialValue
    let { value } = this
    if (value === undefined) {
      value = defaultValue
    }
    const bounds = value.map((v, i) => this.trimAlignValue(v, i))
    const recent = bounds[0] === max ? 0 : bounds.length - 1
    return {
      sHandle: null,
      recent,
      bounds,
    }
  },
  watch: {
    value: {
      handler (val) {
        const { min, max } = this
        this.setChangeValue(val, min, max)
      },
      deep: true,
    },
    min (val) {
      const { bounds, max } = this
      this.setChangeValue(bounds, val, max)
    },
    max (val) {
      const { bounds, min } = this
      this.setChangeValue(bounds, min, val)
    },
  },
  methods: {
    setChangeValue (value, min, max) {
      const { bounds } = this
      const newValue = value || bounds
      const minAmaxProps = {
        min,
        max,
      }
      const nextBounds = newValue.map((v, i) => this.trimAlignValue(v, i, minAmaxProps))
      if (nextBounds.length === bounds.length && nextBounds.every((v, i) => v === bounds[i])) return

      this.setState({ bounds: nextBounds })

      if (bounds.some(v => utils.isValueOutOfRange(v, minAmaxProps))) {
        this.$emit('change', nextBounds)
      }
    },
    onChange (state) {
      const isNotControlled = !hasProp(this, 'value')
      if (isNotControlled) {
        this.setState(state)
      } else if (state.sHandle !== undefined) {
        this.setState({ sHandle: state.sHandle })
      }

      const data = { ...this.$data, ...state }
      const changedValue = data.bounds
      this.$emit('change', changedValue)
    },
    onStart (position) {
      const { bounds } = this
      this.$emit('beforeChange', bounds)

      const value = this.calcValueByPos(position)
      this.startValue = value
      this.startPosition = position

      const closestBound = this.getClosestBound(value)
      const boundNeedMoving = this.getBoundNeedMoving(value, closestBound)

      this.setState({
        sHandle: boundNeedMoving,
        recent: boundNeedMoving,
      })

      const prevValue = bounds[boundNeedMoving]
      if (value === prevValue) return
      const nextBounds = [...bounds]
      nextBounds[boundNeedMoving] = value
      this.onChange({ bounds: nextBounds })
    },
    onEnd () {
      this.setState({ sHandle: null })
      this.removeDocumentEvents()
      this.$emit('afterChange', this.bounds)
    },
    onMove (e, position) {
      utils.pauseEvent(e)
      const props = this.$props
      const { bounds, sHandle } = this
      const value = this.calcValueByPos(position)
      const oldValue = bounds[sHandle]
      if (value === oldValue) return

      const nextBounds = [...bounds]
      nextBounds[sHandle] = value
      let nextHandle = sHandle
      if (props.pushable !== false) {
        this.pushSurroundingHandles(nextBounds, nextHandle)
      } else if (props.allowCross) {
        nextBounds.sort((a, b) => a - b)
        nextHandle = nextBounds.indexOf(value)
      }
      this.onChange({
        sHandle: nextHandle,
        bounds: nextBounds,
      })
    },
    onKeyboard () {
      warning(true, 'Keyboard support is not yet supported for ranges.')
    },
    getClosestBound (value) {
      const { bounds } = this
      let closestBound = 0
      for (let i = 1; i < bounds.length - 1; ++i) {
        if (value > bounds[i]) { closestBound = i }
      }
      if (Math.abs(bounds[closestBound + 1] - value) < Math.abs(bounds[closestBound] - value)) {
        closestBound = closestBound + 1
      }
      return closestBound
    },
    getBoundNeedMoving (value, closestBound) {
      const { bounds, recent } = this
      let boundNeedMoving = closestBound
      const isAtTheSamePoint = (bounds[closestBound + 1] === bounds[closestBound])

      if (isAtTheSamePoint && bounds[recent] === bounds[closestBound]) {
        boundNeedMoving = recent
      }

      if (isAtTheSamePoint && (value !== bounds[closestBound + 1])) {
        boundNeedMoving = value < bounds[closestBound + 1] ? closestBound : closestBound + 1
      }
      return boundNeedMoving
    },
    getLowerBound () {
      return this.bounds[0]
    },
    getUpperBound () {
      const { bounds } = this
      return bounds[bounds.length - 1]
    },
    /**
     * Returns an array of possible slider points, taking into account both
     * `marks` and `step`. The result is cached.
     */
    getPoints () {
      const { marks, step, min, max } = this
      const cache = this._getPointsCache
      if (!cache || cache.marks !== marks || cache.step !== step) {
        const pointsObject = { ...marks }
        if (step !== null) {
          for (let point = min; point <= max; point += step) {
            pointsObject[point] = point
          }
        }
        const points = Object.keys(pointsObject).map(parseFloat)
        points.sort((a, b) => a - b)
        this._getPointsCache = { marks, step, points }
      }
      return this._getPointsCache.points
    },
    pushSurroundingHandles (bounds, handle) {
      const value = bounds[handle]
      let { pushable: threshold } = this
      threshold = Number(threshold)

      let direction = 0
      if (bounds[handle + 1] - value < threshold) {
        direction = +1 // push to right
      }
      if (value - bounds[handle - 1] < threshold) {
        direction = -1 // push to left
      }

      if (direction === 0) { return }

      const nextHandle = handle + direction
      const diffToNext = direction * (bounds[nextHandle] - value)
      if (!this.pushHandle(bounds, nextHandle, direction, threshold - diffToNext)) {
        // revert to original value if pushing is impossible
        bounds[handle] = bounds[nextHandle] - (direction * threshold)
      }
    },
    pushHandle (bounds, handle, direction, amount) {
      const originalValue = bounds[handle]
      let currentValue = bounds[handle]
      while (direction * (currentValue - originalValue) < amount) {
        if (!this.pushHandleOnePoint(bounds, handle, direction)) {
          // can't push handle enough to create the needed `amount` gap, so we
          // revert its position to the original value
          bounds[handle] = originalValue
          return false
        }
        currentValue = bounds[handle]
      }
      // the handle was pushed enough to create the needed `amount` gap
      return true
    },
    pushHandleOnePoint (bounds, handle, direction) {
      const points = this.getPoints()
      const pointIndex = points.indexOf(bounds[handle])
      const nextPointIndex = pointIndex + direction
      if (nextPointIndex >= points.length || nextPointIndex < 0) {
        // reached the minimum or maximum available point, can't push anymore
        return false
      }
      const nextHandle = handle + direction
      const nextValue = points[nextPointIndex]
      const { pushable: threshold } = this
      const diffToNext = direction * (bounds[nextHandle] - nextValue)
      if (!this.pushHandle(bounds, nextHandle, direction, threshold - diffToNext)) {
        // couldn't push next handle, so we won't push this one either
        return false
      }
      // push the handle
      bounds[handle] = nextValue
      return true
    },
    trimAlignValue (v, handle, nextProps = {}) {
      const mergedProps = { ...this.$props, ...nextProps }
      const valInRange = utils.ensureValueInRange(v, mergedProps)
      const valNotConflict = this.ensureValueNotConflict(handle, valInRange, mergedProps)
      return utils.ensureValuePrecision(valNotConflict, mergedProps)
    },
    ensureValueNotConflict (handle, val, { allowCross, pushable: thershold }) {
      const state = this.$data || {}
      const { bounds } = state
      handle = handle === undefined ? state.sHandle : handle
      thershold = Number(thershold)
      /* eslint-disable eqeqeq */
      if (!allowCross && handle != null && bounds !== undefined) {
        if (handle > 0 && val <= (bounds[handle - 1] + thershold)) {
          return bounds[handle - 1] + thershold
        }
        if (handle < bounds.length - 1 && val >= (bounds[handle + 1] - thershold)) {
          return bounds[handle + 1] - thershold
        }
      }
      /* eslint-enable eqeqeq */
      return val
    },
    getTrack ({ bounds, prefixCls, vertical, included, offsets, trackStyle }) {
      return bounds.slice(0, -1).map((_, index) => {
        const i = index + 1
        const trackClassName = classNames({
          [`${prefixCls}-track`]: true,
          [`${prefixCls}-track-${i}`]: true,
        })
        return (
          <Track
            class={trackClassName}
            vertical={vertical}
            included={included}
            offset={offsets[i - 1]}
            length={offsets[i] - offsets[i - 1]}
            style={trackStyle[index]}
            key={i}
          />
        )
      })
    },
    renderSlider () {
      const {
        sHandle,
        bounds,
        prefixCls,
        vertical,
        included,
        disabled,
        min,
        max,
        handle: handleGenerator,
        trackStyle,
        handleStyle,
        tabIndex,
        $createElement,
      } = this

      const offsets = bounds.map(v => this.calcOffset(v))

      const handleClassName = `${prefixCls}-handle`
      const handles = bounds.map((v, i) => handleGenerator($createElement, {
        className: classNames({
          [handleClassName]: true,
          [`${handleClassName}-${i + 1}`]: true,
        }),
        vertical,
        offset: offsets[i],
        value: v,
        dragging: sHandle === i,
        index: i,
        tabIndex: tabIndex[i] || 0,
        min,
        max,
        disabled,
        style: handleStyle[i],
        ref: 'handleRefs_' + i,
        handleFocus: this.onFocus,
        handleBlur: this.onBlur,
      }))

      return {
        tracks: this.getTrack({ bounds, prefixCls, vertical, included, offsets, trackStyle }),
        handles,
      }
    },
  },
}

export default createSlider(Range)
